/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.google.code.twiddling.core.clp;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;

import com.google.code.twiddling.core.clp.handler.Handlers;
import com.google.code.twiddling.core.clp.setter.CollectionFieldSetter;
import com.google.code.twiddling.core.clp.setter.FieldSetter;
import com.google.code.twiddling.core.clp.setter.MethodSetter;
import com.google.code.twiddling.core.clp.setter.Setter;
import com.google.code.twiddling.core.exception.TwiddlingUncheckedException;
import com.google.code.twiddling.core.log.LogUtils;
import com.google.code.twiddling.core.log.Message;

/**
 * @author <a href="mailto:jeff.yuchang@gmail.com">Jeff Yu</a>
 *
 */
public class CommandLineParser {
	
	private List<Handler<?>> optionHandlers = new ArrayList<Handler<?>>();
	
	private List<Handler<?>> argumentHandlers = new ArrayList<Handler<?>>();
	
	private boolean stopAtNonOption = false;
	
	private static final Logger logger = LogUtils.getL7dLogger(CommandLineParser.class);
	
	private static final ResourceBundle bundle = logger.getResourceBundle();
	
	public CommandLineParser(Object bean) {
    	if (bean == null) {
        	throw new NullPointerException(new Message("BEAN_CANT_BE_NULL", bundle, new Object[]{bean}).toString());
        }
    	
		discoverDescriptors(bean);
	}
	
	
   /**
    * 
    * 
    * // TODO: Need to rewrite some of this to allow more posix-style argument processing, 
    * //  like --foo=bar and --foo bar, and -vvvv
    * @param args
    * @throws CLParsingException
    */
   public void process(final String... args) throws CLParsingException {
       
	   checkIfVerboseOutput(args);
	   
	   DefaultParameter params = new DefaultParameter(args);
       Set<Handler<?>> present = new HashSet<Handler<?>>();
       int argIndex = 0;
       boolean processOptions = true;
       boolean requireOverride = false;
             
       while (params.hasMore()) {
            String arg = params.current();
            Handler<?> handler;

            if (processOptions && arg.startsWith("-")) {
            	boolean isKeyValuePair = ( arg.indexOf('=') != -1);

                // parse this as an option.
                handler = isKeyValuePair ? findOptionHandler(arg) : findOptionByName(arg);

                if (handler == null) {
                    if (stopAtNonOption) {
                        // Slurp up the remaining bits as arguments (including the option we just looked at)
                        processOptions = false;
                        continue;
                    }
                    else {
                        // Unknown option, complain
                        throw new CLParsingException(new Message("UNKNOWN_OPTION", bundle, new Object[]{arg}).toString());
                    }
                }
                else if (isKeyValuePair){
                	// known option, but further processing is required in the handler.
                	handler.isKeyValuePair = isKeyValuePair;
                } else {
                    // known option; skip its name
                    params.skip(1);
                }
            }
            else {
                // Complain if we have more arguments than we have handlers configured
                if (argIndex >= argumentHandlers.size()) {
                	String msg = "TOO_MANY_ARGUMENTS";
                	if (argumentHandlers.size() == 0) {
                		msg = "NO_ARGUMENTS_ALLOWED";
                	}
                    throw new CLParsingException(new Message(msg, bundle).toString());
            	}

            	// known argument
            	handler = argumentHandlers.get(argIndex);
            	if (!handler.descriptor.isMultiValued()) {
                    argIndex++;
                }
            }

            try {
                //Hook up the current handler to the params for error message rendering
                params.setHandler(handler);

                // If this is an option which overrides requirements track it
                if (!requireOverride && handler.descriptor instanceof OptionDescriptor) {
                    OptionDescriptor d = (OptionDescriptor) handler.descriptor;
                    requireOverride = d.isRequireOverride();
                }
                
                // Invoker the handler and then skip arguments which it has eaten up
                int consumed = handler.handle(params);
                params.skip(consumed);
            }
            catch (StopProcessingOptionsNotification n) {
                processOptions = false;
            }

            // Keep a list of the handlers which have been processed (for required validation below)
            present.add(handler);
        }
        
        //See if it is like 'info -h' to show the command help document.
        boolean helpShower = false;
        if (args.length == 1  && ((("-h".equals(args[0])) || ("--help".equals(args[0]))))) {
        	helpShower = true;
        }
        
        // Ensure that all required option handlers are present, unless a processed option has overridden requirments
        if (requireOverride == false && helpShower == false) {
	        checkAllRequiredOptions(present);	        
	        checkAllRequiredArguments(present);
        }
    }

   /**
    * Check if the parameter turns on 'verbose' mode or not.
    * @param args
    * @throws CLParsingException
    */
	private void checkIfVerboseOutput(final String... args) throws CLParsingException {
		DefaultParameter theParameters = new DefaultParameter(args);
		while (theParameters.hasMore()) {
			String arg = theParameters.current();
			if ("-v".equals(arg) || "--verbose".equals(arg)) {
				Handler<?> handler = findOptionByName(arg);
				handler.handle(theParameters);
			}
			theParameters.skip(1);
		}
	}

   /**
    * Ensure that all required option handlers are present
    * @param present
    * @throws CLParsingException
    */
	private void checkAllRequiredOptions(Set<Handler<?>> present)
			throws CLParsingException {
		for (Handler<?> handler : optionHandlers) {
		    if (handler.descriptor.isRequired() && !present.contains(handler)) {
		        throw new CLParsingException(new Message("OPTION_IS_REQUIRED", bundle, new Object[]{handler.descriptor.toString()}).toString());
		    }
		}
	}

    /**
     * Ensure that all required argument handlers are present
     * @param present
     * @throws CLParsingException
     */
	private void checkAllRequiredArguments(Set<Handler<?>> present) throws CLParsingException {
		for (Handler<?> handler : argumentHandlers) {
		    if (handler.descriptor.isRequired() && !present.contains(handler)) {                
		        throw new CLParsingException(new Message("ARGUMENT_IS_REQUIRED", bundle, new Object[]{handler.descriptor.toString()}).toString());
		    }
		}
	}
	
    private void discoverDescriptors(final Object bean) {

        // Recursively process all the methods/fields.
        for (Class<?> type=bean.getClass(); type!=null; type=type.getSuperclass()) {
            // Discover methods
            for (Method method : type.getDeclaredMethods()) {
                Option option = method.getAnnotation(Option.class);
                if (option != null) {
                    addOption(new MethodSetter(bean, method), option);
                }

                Argument argument = method.getAnnotation(Argument.class);
                if (argument != null) {
                    addArgument(new MethodSetter(bean, method), argument);
                }
            }

            // Discover fields
            for (Field field : type.getDeclaredFields()) {
                Option option = field.getAnnotation(Option.class);
                if (option != null) {
                    addOption(createFieldSetter(bean, field), option);
                }

                Argument argument = field.getAnnotation(Argument.class);
                if (argument != null) {
                    addArgument(createFieldSetter(bean, field), argument);
                }
            }
        }

        // Sanity check the argument indexes
        for (int i=0; i< argumentHandlers.size(); i++) {
            if (argumentHandlers.get(i) == null) {
                throw new TwiddlingUncheckedException(new Message("NO_ARGUMENT_ANNOTATION", bundle, new Object[]{i}).toString());
            }
        }
    }
	
    
    private void addOption(final Setter<?> setter, final Option option) {
        Handler<?> handler = Handlers.create(new OptionDescriptor(setter.getName(), option, setter.isMultiValued()), setter);
        checkOptionNotInMap(option.name());

        for (String alias : option.aliases()) {
            checkOptionNotInMap(alias);
        }

        optionHandlers.add(handler);
    }
    
    private void addArgument(final Setter<?> setter, final Argument argument) {
        Handler<?> handler = Handlers.create(new ArgumentDescriptor(setter.getName(), argument, setter.isMultiValued()), setter);
    	int index = argument.index();

        // Make sure the argument will fit in the list
    	while (index >= argumentHandlers.size()) {
            argumentHandlers.add(null);
    	}

        if (argumentHandlers.get(index) != null) {
            throw new IllegalAnnotationException(new Message("DUPLICATED_ARGUMENT_INDEX", bundle, new Object[]{index}).toString());
        }

        argumentHandlers.set(index, handler);
    }
    
    private void checkOptionNotInMap(final String name) throws IllegalAnnotationException {
        if (findOptionByName(name) != null) {
            throw new IllegalAnnotationException(new Message("DUPLICATED_OPTION_NAME", bundle, new Object[]{name}).toString());
        }
    }
    
    private Handler<?> findOptionHandler(String name) {
        Handler<?> handler = findOptionByName(name);

        if (handler == null) {
            // Have not found by its name, maybe its a property?
            // Search for parts of the name (=prefix) - most specific first 
            for (int i=name.length(); i>1; i--) {
                String prefix = name.substring(0, i);
                Map<String, Handler<?>> possibleHandlers = filter(optionHandlers, prefix);
                handler = possibleHandlers.get(prefix);

                if (handler != null) {
                    return handler;
                }
            }
        }

        return handler;
    }
    
    private Handler<?> findOptionByName(String name) {
        for (Handler<?> handler : optionHandlers) {
            OptionDescriptor option = (OptionDescriptor)handler.descriptor;

            if (name.equals(option.getName())) {
                return handler;
            }

            for (String alias : option.getAliases()) {
                if (name.equals(alias)) {
                    return handler;
                }
            }
        }

        return null;
    }
    
    private Map<String, Handler<?>> filter(List<Handler<?>> handlers, String keyFilter) {
        Map<String, Handler<?>> map = new TreeMap<String, Handler<?>>();

        for (Handler<?> handler : handlers) {
        	if (keyFilter.contains("--")) {
        		for (String alias : ((OptionDescriptor)handler.descriptor).getAliases()) {
        			if (alias.startsWith(keyFilter)) {
        				map.put(alias, handler);
        			}
        		}
        	} else {
        		if (((OptionDescriptor)handler.descriptor).getName().startsWith(keyFilter)) {
                    map.put(((OptionDescriptor)handler.descriptor).getName(), handler);
                }
        	}
        }

        return map;
    }
    
    private Setter<?> createFieldSetter(final Object bean, final Field field) {

        if (Collection.class.isAssignableFrom(field.getType())) {
            return new CollectionFieldSetter(bean, field);
        }
        else {
            return new FieldSetter(bean, field);
        }
    }

	public List<Handler<?>> getOptionHandlers() {
		return optionHandlers;
	}

	public List<Handler<?>> getArgumentHandlers() {
		return argumentHandlers;
	}

	public boolean isStopAtNonOption() {
		return stopAtNonOption;
	}

	public void setStopAtNonOption(boolean stopAtNonOption) {
		this.stopAtNonOption = stopAtNonOption;
	}
	
}
